Blog

WebAssembly – auch für Angular-Anwendungen

Sep 15, 2023

Mit WebAssembly, kurz „Wasm“, lassen sich in Webanwendungen rechenintensive Features umsetzen, die dann mit (nahezu) derselben Geschwindigkeit wie nativer Code laufen. Gleichzeitig können mit Hilfe der Technologie viele bestehende Bibliotheken, die in anderen Programmiersprachen wie C oder C++ geschrieben sind, für den Browser verfügbar gemacht werden. Wir sehen uns in diesem Beitrag an, wie die Integration von Wasm in eine Angular-Anwendung technisch ablaufen kann und worauf es zu achten gilt.

Zum einen kann mit Wasm anspruchsvolle Logik implementiert werden, die dann das Potenzial der darunterliegenden Hardware besser nutzen kann, als das in JavaScript der Fall wäre.

Einsatzbereiche von WebAssembly

Bereits jetzt besteht die Möglichkeit, SIMD-Instruktionen in WebAssembly zu verwenden und so in nur einem CPU-Zyklus mehrere Datenelemente gleichzeitig zu bearbeiten. Damit lassen sich – je nachdem, ob die zu implementierenden Berechnungen zu diesem Modell passen – Operationen um mehrere Faktoren beschleunigen.

Zum anderen ist es mit WebAssembly möglich, existierenden, nicht auf JavaScript basierenden Bibliotheken für komplexe Probleme wie algorithmische Berechnungen, Grafikverarbeitung und Machine Learning den Weg in den Browser zu öffnen. Dadurch lässt sich im Browser umsetzen, was bisher nativen Anwendungen vorbehalten waren. Beispiele sind Videokonferenzsoftware und Kollaborationslösungen. Ein guter Einsatzbereich für WebAssembly ist etwa, den Hintergrund in einer Videoübertragung durch eine alternative Grafik zu ersetzen oder weichzuzeichnen. Dazu muss die Kontur der Person erkannt und anschließend vom Hintergrund separiert werden. Typischerweise wird diese Aufgabe mit einem neuronalen Netz und Kantenerkennung umgesetzt. Ein für SIMD gut passender Anwendungsfall.

Auch andere technisch anspruchsvolle Features wie Grafikbearbeitung oder Umsetzung von komplexen Algorithmen zur Konfliktauflösung bei der Kollaboration mehrerer Personen, wie sie beispielsweise in Google Docs verwendet wird, sind oft bereits in einer Sprache implementiert, die nicht direkt durch den Browser unterstützt wird. Ist es aber möglich, von dieser Sprache zu WebAssembly zu kompilieren, kann das Ergebnis nun ohne aufwendige Portierung direkt im Browser eingebunden und genutzt werden. Nahezu alle Sprachen unterstützen mittlerweile die Ausgabe von WebAssembly.

ANGULAR LIVE IN ACTION?

Die Angular-Workshops vom 21. - 24. Oktober 2024 entdecken

Angular-Strukturen für komplexe Anwendungen

Im Gegensatz zu reinen Komponentenbibliotheken, wie beispielsweise React, liefert Angular wichtige architektonische Konzepte direkt mit, um auch komplexe Anwendungen sinnvoll modularisieren zu können. So gibt es in Angular das Konzept eines Service: Damit können unter anderem Bestandteile der Anwendung umgesetzt werden, die sich mit Datenhaltung, Datenbeschaffung oder Datentransformation beschäftigen. Mit Services werden zum Beispiel Backends via HTTP mit dem Angular HttpClient oder auch per WebSocket angebunden. Angular verfügt auch über einen Dependency-Injection-Mechanismus. Damit können in der Anwendung sehr einfach Abhängigkeiten definiert werden, ohne dass eine entsprechende Infrastruktur gebaut werden muss, welche die Abhängigkeiten für den jeweiligen Konsumenten bereitstellt. Dabei gibt es einen wichtigen Aspekt: Das Angular-Dependency-Injection-System instanziiert standardmäßig jede Abhängigkeit lediglich einmal. Es handelt sich also effektiv um Singletons, ohne dass das Singleton-Muster dabei implementiert werden muss. Damit ist im Kontext von WebAssembly auf recht einfache Weise sicherzustellen, dass nicht mehrere Instanzen einer ggf. sehr ressourcenintensiven Bibliothek erstellt werden.

Komponenten in Angular dienen schließlich dazu, die Integration mit den für den Nutzer relevanten Browserbestandteilen vorzunehmen. Zum einen können Komponenten durch DOM-Bearbeitung die gewünschte Anzeige, wie z. B. den Inhalt eines Labeltexts, sicherstellen. Zum anderen sind Komponenten auch dafür zuständig, Benutzereingaben in Form von Input-Bindings und Output-Events zu verarbeiten. Klickt ein Nutzer z. B. auf einen Knopf auf der Oberfläche, kann eine Angular-Komponente einen Listener auf das zugehörige Klick-Event registriert haben und daraufhin entsprechende Logik ausführen.

Ergänzend dazu liefert Angular ein eigenes Modulsystem, das die Umsetzung einer gut wartbaren und übersichtlichen Architektur innerhalb der Anwendung unterstützt. So erlaubt es das Modulsystem, zweifelsfrei zu definieren, welche Komponenten für andere Module sichtbar sind. Damit ist eine klar definierte API-Oberfläche möglich und unbeabsichtigte Kopplungen, die sich bei späteren Erweiterungen und Refactorings als Aufwandstreiber oder sogar Show-Stopper herausstellen könnten, werden reduziert. Außerdem bietet Angular APIs, um Module oder auch Komponenten lazy zu laden, also erst bei Bedarf. Das ist ein wichtiger Baustein zur Umsetzung von schnell geladenen und performanten Anwendungen; es müssen nicht alle Features auf Vorrat geladen und initialisiert werden. Weitere Optimierungen wie Lazy Preloading können ebenfalls eingesetzt werden, sodass der initiale Anwendungsstart sehr performant erfolgt und später benötigte Features verdeckt im Hintergrund geladen werden, um sofort zur Verfügung zu stehen, wenn sie vom Nutzer aufgerufen werden.

Soll nun durch WebAssembly eine Funktionalität für die Angular-Anwendung bereitgestellt werden, bietet sich eben der Einsatz eines Service an. Damit steht das Feature per Dependency Injection in der gesamten Anwendung zur Verfügung. Gleichzeitig ist dadurch sichergestellt, dass alle Anwendungskomponenten auf denselben Daten und mit derselben Instanz des Service arbeiten. Damit kann z. B. dieselbe Instanz einer WebAssembly-Funktionalität von unterschiedlichen Komponenten bei der Anzeige genutzt werden. Ein Beispiel dafür könnte die Anzeige einer Simulation in Form einer Übersicht mit niedriger Auflösung und eine Ausschnittansicht der Simulation mit hoher Auflösung sein.

Zum Einbinden von Wasm in eine Webanwendung ist immer auch ein gewisser Anteil an Glue-Code notwendig. Er übernimmt die Kommunikation mit dem Wasm-Code, indem beispielsweise Zeichenketten (de-)kodiert werden oder bei der Instanziierung über Import- und Exportfunktionen die Integration in die umgebende Anwendung konfiguriert wird. Bei einer Angular-Anwendung liefert der Glue-Code dann auch die Angular-spezifische Abstraktion als Modul bzw. Service. Dieser Glue-Code ist typischerweise ein wenig unterschiedlich, je nach Funktionalität, die der Wasm-Code zur Verfügung stellt, und je nach verwendetem Build-Tool bzw. der Sprache, mit der der Wasm-Code erzeugt wurde.

Gerade, wenn mehrere Anwendungen mit Wasm-Funktionalität ausgestattet werden müssen, bietet es sich in der Regel an, den Glue-Code in eine Library auszulagern, die dann von den Projekten eingebunden werden kann. Bei firmeninternen Anwendungslandschaften ist es entsprechend sinnvoll, diese Library dann z. B. in einem firmeninternen Artifact bzw. npm-Repository unterzubringen, um sie firmenweit wiederverwenden zu können.

Beispiel mit AssemblyScript

Da WebAssembly aus verschiedenen Sprachen generiert werden kann, bietet es sich an, die Sprache zu nutzen, die zum jeweils eigenen Anwendungsfall passt. Zum Starten kann beispielsweise AssemblyScript genutzt werden [1]. Dabei handelt es sich um eine mit TypeScript verwandte Sprache, deshalb wird AssemblyScript Webentwicklern vertraut vorkommen. Auch wenn es sich bei AssemblyScript um eine eigenständige Sprache handelt, kann man sie sich auch als Untermenge von TypeScript vorstellen.

Die Installation ist einfach und setzt lediglich eine Installation von Node und npm voraus. Dann kann ein neuer Projektordner angelegt werden, in dem die folgenden Befehle ausgeführt werden müssen:

npm init
npm install --save-dev assemblyscript
npx asinit .

Dadurch wird ein Projekt mit einer Struktur entsprechend Abbildung 1 erzeugt. Der assembly-Ordner enthält den Quellcode, während der build-Ordner die kompilierten Wasm-Skripte enthält. Zum Kompilieren nach Wasm muss dann nur noch der Build mit npm run asbuild gestartet werden.

Abb. 1: Generierte Projektstruktur eines AssemblyScript-ProjektsAbb. 1: Generierte Projektstruktur eines AssemblyScript-Projekts

Hier ein Codebeispiel in AssemblyScript, das eine Addition implementiert:

export function add(a: i32, b: i32): i32 {
  return a + b;
}

Beim Kompilieren werden aus diesem Code nun mehrere Dateien erzeugt. Ein solches Build-Resultat zeigt Abbildung 2. Für uns ist hier besonders die Datei release.wasm von Interesse. In diese Datei hinein wurde die AssemblyScript-Funktion im Wasm-Format kompiliert. Um die Datei einfach aus unserem Angular-Projekt heraus aufrufen zu können, können wir sie etwa in den assets-Ordner unserer Angular-App kopieren. Eine spätere entsprechende Automatisierung der Build-Schritte ist hier offensichtlich angeraten.

Abb. 2: Dateien der Build-Ausgabe des AssemblyScript-ProjektsAbb. 2: Dateien der Build-Ausgabe des AssemblyScript-Projekts

Gemeinsam sind wir stark

Um die Wasm-Logik aus Angular heraus anzubinden, bietet es sich an, einen Service für den Glue-Code zu schreiben. Das hat den Vorteil, dass die Wasm-Instanz nicht direkt an den Lebenszyklus einer Angular-Komponente gebunden wird. Ein solcher Wasm-Service ist zum Beispiel in Listing 1 dargestellt. Dort wird die add-Funktion aus dem AssemblyScript als privates Property wasmAdd gehalten. Bevor wir die add-Funktion jedoch nutzen können, muss der Service zunächst initialisiert werden. Das geschieht in der init()-Funktion, die aufgrund des einfachen Beispiels hier direkt im Konstruktor aufgerufen wird. Weitere Optimierungen werden weiter unten besprochen. In der init()-Funktion wird zuerst die Wasm-Datei per fetch-Request geladen. Es könnte prinzipiell auch der Angular HttpService genutzt werden. Mit dem fetch-Ansatz kann die Wasm-Instanz allerdings etwas einfacher erzeugt werden, da vom WebAssembly-Browser-API direkt die Funktion WebAssembly.instantiateStreaming() zur Verfügung gestellt wird, die direkt die fetch-Response zum Erzeugen der Wasm-Instanz nutzt. Mit dem Angular HttpClient geladene Wasm-Dateien müssen manuell in einen Buffer verwandelt und dann zum Instanziieren an WebAssembly übergeben werden. Da wir im Beispiel jedoch das fetch-API verwenden, können wir alternativ auch die fetch-Response direkt an WebAssembly weiterreichen. Aus dem so erhaltenen WebAssembly-Modul können wir nun über die WebAssembly-Instanz und aus deren Exports die add()-Funktion referenzieren und an this.wasmAdd binden. Da Wasm jedoch in der Regel keine konkreten Typings mitliefert, hat TypeScript zunächst keine spezielleren Typinformationen über die Wasm-Exports. Daher setzen wir an dieser Stelle eine Type Assertion ein, um den exakten Typ der Funktion zu korrigieren. Danach kann einfach die add()-Funktion des Service aufgerufen werden, die dann direkt die Wasm-Funktion aufruft.

Listing 1: Angular-Service zum Aufruf von Wasm-Funktionen

@Injectable({providedIn: 'root'})
export class WasmService {
  private wasmAdd?: (valA: number, valB: number) => number;
 
  constructor() { this.init(); }
 
  async init(): Promise<void> {
    const wasmBlob = fetch('assets/release.wasm');
    const module = await WebAssembly.instantiateStreaming(wasmBlob);
    const exports = module.instance.exports;
    this.wasmAdd = exports['add'] as (valA: number, valB: number) => number;
  }
  
  add(valueA: number, valueB: number): number {
    if (!this.wasmAdd) {
      throw new Error('Wasm Add not initialized');
    }
    return this.wasmAdd(valueA, valueB);
  }
}

Der Service kann dann wie ein gewöhnlicher Angular-Service verwendet werden. In Listing 2 ist exemplarisch eine Angular-Komponente gezeigt, die ein Formular enthält. In das Formular können in zwei unterschiedliche Inputfelder jeweils Zahlen eingetragen werden. Die Inputfelder sind mit einer Angular-Reactive-formGroup verbunden. Jedes Mal, wenn sich – z. B. durch eine Nutzereingabe – der Wert in einem der Felder ändert, wird ein valueChange gefeuert. Die momentanen Input-Werte werden an den WasmService weitergeleitet, der die Berechnung in Wasm anstößt. Das Resultat dieser Berechnung landet schließlich in der Variablen result, die im Template ausgegeben wird. Im Beispiel aus Listing 2 wird dafür die async-Pipe verwendet, da es sich bei result um ein asynchrones Observable handelt, das immer genau dann aktualisiert wird, wenn sich einer der beiden Formularwerte ändert.

Listing 2: Angular-Komponente, die Wasm-Funktionalität verwendet

@Component({
  selector: 'app-add',
  template: '
    <div [formGroup]="calcForm">
      <div>
        <label>Wert A</label>
        <input type="number" formControlName="valueA">
      </div>
      <div>
        <label>Wert B</label>
        <input type="number" formControlName="valueB">
      </div>
    </div>
    <p>Ergebnis: {{result|async}}</p>
  '
})
export class AddComponent {
  readonly calcForm = new FormGroup({
    valueA: new FormControl(0, {nonNullable: true}),
    valueB: new FormControl(0, {nonNullable: true})
  });
  result = this.calcForm.valueChanges
    .pipe(
      map(() => {
        const values = this.calcForm.getRawValue();
        return this.wasm.add(values.valueA, values.valueB);
    })
  );
 
  constructor(private readonly wasm: WasmService) {}
}

Optimierungen beim Laden

Es kann durchaus vorkommen, dass Wasm-Dateien im Vergleich zu JavaScript sehr groß werden, z. B., wenn komplexe Simulationen oder große Datenmodelle per HTTP geladen werden müssen. In einem solchen Fall muss der Nutzer natürlich auf eventuelle Wartezeiten aufmerksam gemacht werden. Typischerweise wird dazu ein Ladeindikator (z. B. Spinner) angezeigt. Um diesen Indikator zu steuern, kann ausgenutzt werden, dass das WebAssembly API mit Promises arbeitet.

In Listing 3 ist ein WasmService zu sehen, der eine generische init()-Funktion hat, die die Exports der Wasm-Instanz in Form eines Promise zurückliefert. Diese Funktion wird von der Komponente in Listing 4 aufgerufen, um die Berechnung einer Fibonacci-Folge per Wasm durchführen zu lassen (der Code zur Berechnung einer Fibonacci-Folge ist zwar weder sehr komplex noch sehr groß, soll hier aber auch nur als Platzhalter für eine umfangreichere Logik fungieren).

Da die Funktion in ein Promise gewrappt ist, kann die Angular-Async-Pipe verwendet werden, um im Template so lange den Ladebildschirm anzuzeigen, bis die Wasm-Datei geladen und die Wasm-Instanz erzeugt wurde.

Listing 3: Angular-Service, um generische Wasm-Funktion zu laden

@Injectable({providedIn: 'root'})
export class WasmService {
 
  init(filename: string): Promise<WebAssembly.Exports> {
    const request = fetch('assets/' + filename)
    return WebAssembly.instantiateStreaming(request)
      .then(module => module.instance.exports);
  }
}

Listing 4: Laden einer Wasm-Funktionalität aus einer Komponente

@Component({
  selector: 'app-fib,
  template: '
    <div *ngIf="wasmFib|async; else loading" class="card-container">
      <button (click)="startWasmFib(25)">Start</button>
      <div>Result: {{result}}</div>
    </div>
    <ng-template #loading><div>Loading...</div></ng-template>
  '
})
export class FibonacciComponent {
  result = 0;
  readonly wasmFib: Promise<(num: number) => number>;
 
  constructor(private readonly wasmUtil: WasmService) {
    this.wasmFib = this.wasmUtil.init('fibonacci.wasm')
      .then(exports => exports['fibonacci'] as (num: number) => number);
  }
 
  startWasmFib(ordnung: number): void {
    this.wasmFib.then(fibonacci => {
      result = fibonacci(ordnung);
    });
  }
}

Angular-Modulsystem als Architekturpfeiler

Das Angular-Modulsystem bietet die Möglichkeit, eine Anwendung in einzelne und isolierte Fachlichkeiten zu zerlegen bzw. von Anfang an so zu entwickeln. Module können separat geladen werden. Das kann zum einen genutzt werden, um die subjektive Ladegeschwindigkeit der Anwendung zu optimieren. Auf der anderen Seite bieten sich Module an, um Funktionalität, die z. B. zur Wiederverwendung ausgelagert werden soll, zu einem gemeinsamen Bündel zu aggregieren. Damit kann ein Modul als Fassade dienen, die Komplexität der Implementierung verbergen und eine reduzierte bzw. vereinfachte API-Oberfläche anbieten.

Ein Modul kann dabei wiederum andere Module referenzieren und sowohl Services als auch Komponenten oder Direktiven beinhalten. Ein Beispiel für ein mögliches Angular-Modul wäre eine Funktionalität wie Google Maps, die in verschiedenen Bereichen einer Anwendung oder sogar verschiedenen Anwendungen eingesetzt werden kann. Jedes Modul verfügt selbst über einen Konstruktor, mit dem Initialisierungen koordiniert werden können, falls erforderlich.

Module können als Bibliotheken über ein Artefakt-Repository verteilt werden. Damit sind die gemeinsame Versionierung und der Test der Bestandteile möglich. Entsprechend reduziert sich die Menge der zu pflegenden Versionen und auch der zu testenden Permutationen. Im Fall einer Wasm-basierten Funktionalität können Module genutzt werden, um als Abstraktionsebene zugrunde liegende Komponenten und Services zusammenzufassen und versioniert bereitzustellen. Wird innerhalb eines Unternehmens mit Angular gearbeitet, können damit Teams auf entsprechend hochwertige Artefakte zurückgreifen und müssen nicht redundant für ihren jeweiligen Kontext die Einbettung in Angular vornehmen.

Auch wenn in jüngster Zeit der Trend dahin geht, das Modulsystem nicht mehr so sehr in den Fokus zu rücken, so bietet es sich zum Beispiel an, wenn in sich geschlossene Features zur Verfügung gestellt werden sollen, die neben Services auch aus mehreren Komponenten bestehen. Das kann natürlich auch im Kontext von Wasm Sinn ergeben. Alternativ gibt es bei einfacheren Anwendungsfällen, in denen zum Beispiel nur der Glue-Code in die Library ausgelagert werden soll, die Möglichkeit, auch nur einen Service oder globale Funktionen in eine Library auszulagern. Ein Entscheidungskriterium könnte sein, ob der Fokus eher darauf liegt, generischen Wasm-Code für Angular bereitzustellen, oder ob es sich um eine umfangreichere Teilfunktionalität handelt, die neben Wasm bereits mehrere Komponenten zur Darstellung und Interaktion mitbringt. Ein weiteres Entscheidungskriterium ist natürlich, ob das Angular-Modulsystem auch sonst verwendet wird oder nicht.

Eine neue Library mit dem Namen wasm-lib kann innerhalb eines bestehenden Angular-Projekts durch Eingabe des Befehls ng generate lib wasm-lib erzeugt werden. In das Library-Projekt können dann die Funktionen, Services oder auch Komponenten, die zu dem auszulagernden Feature gehören, verschoben werden. Das Projekt kann dann per ng build wasm-lib gebaut und mit Hilfe von npm in eine Registry gepublisht werden. In anderen Projekten wird die Library ganz normal als versionierte Abhängigkeit über die package.json ausgedrückt.

Aufgepasst, es gibt Changes

Da Wasm mit nahezu nativer Performance läuft, eignet es sich unter anderem für rechenintensive Aufgaben. Der Aufruf der Wasm-Funktionen, inklusive Datentransfer von und zu Wasm, erfolgt synchron. Daher kann es passieren, dass der Wasm-Aufruf bei sehr rechenintensiven Tasks den Browser längere Zeit blockiert. Das ist natürlich schlecht für die Performance und insbesondere schlecht für die Nutzbarkeit der Seite, die das betreffende Wasm-Skript einbindet. Auch diese Aspekte sollten im Entwicklungsprozess der jeweiligen Anwendung immer bedacht werden.

Eine Möglichkeit, mit einem solchen sehr rechenintensiven Skript umzugehen, sind Web Worker. Mit ihnen ist es möglich, einen weiteren Thread im Browser zu starten, um etwa solche rechenintensiven Tasks auszuführen. Tatsächlich ist es auch möglich, WebAssembly aus Web Workern heraus anzustoßen.

Bleibt noch die Frage nach der Integration von Web Workern mit Angular. Auch hier wird Entwicklern bereits alles Notwendige zur Verfügung gestellt, sodass man es direkt anwenden kann. Angular erlaubt es sogar, neue Web Worker per Schematic anzulegen. Dazu muss einfach nur der Befehl ng generate web-worker wasm-demo innerhalb eines Angular-Projekts eingegeben werden. In diesem Fall wird also ein Web Worker Wasm-Demo angelegt. Den Web Worker können wir dann zum Beispiel aus unserer Fibonacci-Komponente heraus anbinden, wie es Listing 5 zeigt. Dafür muss der Worker als new Worker() instanziiert werden, und es muss ein relativer URL zur Web-Worker-TypeScript-Datei angegeben werden, damit auch der Worker von TypeScript kompiliert werden kann. Um die Daten, die im Web Worker durch Wasm errechnet werden, in der Komponente nutzen zu können, muss ein Listener am message Event des Workers registriert werden, da ein Web Worker nur über diesen Mechanismus mit dem Haupt-Thread kommunizieren kann. Wir können dem Web Worker dann per postMessage Daten (oder sogar ein sogenanntes Offscreen Canvas) hineinreichen, mit denen innerhalb des Web Worker die Wasm-Funktion aufgerufen wird. So kann ein Web Worker genutzt werden, um Wasm-Berechnungen effektiv parallel zum Brower-UI-Thread auszuführen.

Listing 5: Komponente, die den Web Worker aufruft

@Component({})
export class FibonacciComponent implements OnDestroy {
  results = '';
  private wasmWorker = new Worker(
    new URL('./wasm-demo.worker', import.meta.url), 
    {type: 'module'}
  );
 
  constructor() {
    this.wasmWorker.addEventListener('message', event => {
      this.results = event.data
    });
  }
 
  startWebWorkerWasm(ordnung: number): void {
    this.wasmWorker.postMessage(ordnung);
  }
 
  ngOnDestroy(): void {
    this.wasmWorker.terminate();
  }
}

Der Web Worker selbst könnte zum Beispiel aussehen, wie in Listing 6 dargestellt. Zunächst wird auch in diesem per fetch die Wasm-Datei geladen, die dann zur Instanziierung der WebAssembly-Umgebung genutzt wird. Das Promise, das von der instantiateStreaming()-Funktion zurückgeliefert wird, wird hier mit der then()-Syntax aufgelöst, generell kann aber je nach Vorliebe auch die async-await-Syntax genutzt werden.

Der wichtigste Bestandteil dieses Web Worker ist die addEventListener()-Funktion, die man nutzen kann, um auf Message-Events von außen zu horchen. Sobald ein solches Event kommt, werden daraus die Daten extrahiert (data) und an die Wasm-Fibonacci-Funktion weitergeleitet. Das Ergebnis wird dann wiederum per postMessage() an die Komponente im Haupt-Thread zurückgereicht.

Listing 6: Web Worker, der Wasm-Funktion anbindet

/// <reference lib="webworker" />
 
const wasmReq = fetch('assets/release.wasm');
const wasmFib = WebAssembly.instantiateStreaming(wasmReq)
  .then(wasmFibonacci => wasmFibonacci.instance.exports)
  .then(exports => exports['fibonacci'] as (num: number) => number);
 
addEventListener('message', ({data}) => {
  wasmFib
    .then(fibonacci => fibonacci(data))
    .then(result => postMessage(result));
});

Anspruchsvolleres Beispiel: Videohintergrund

Um das Potenzial von Wasm und gerade von SIMD-Operationen zu verdeutlichen, werfen wir zum Abschluss einen Blick auf ein anspruchsvolleres Beispiel. Es ist vollständig unter [2] auf GitHub zu finden. Es handelt sich um eine Angular-Anwendung, die per getUserMedia() einen Livestream der Webcam erhält. Dabei kann durch einen Schalter, der als Angular-Komponente umgesetzt ist, das Feature Hintergrund weichzeichnen aktiviert werden. Das Feature verwendet Tensorflow-Lite [3], das als Wasm-Kompilat im Projekt bereitsteht. Dabei wird auch von SIMD Gebrauch gemacht, um die Operationen maximal effizient auszuführen. Die Verarbeitung könnte als Optimierung durch einen Web Worker erfolgen, der zum einen die Wasm-Funktionen aufruft und zum anderen direkt in das zugehörige Canvas zeichnet. Damit würde die zusätzliche Kommunikation mit dem Haupt-Thread der Anwendung vermieden, was dann noch einmal zu mehr Effizienz beitragen würde. Falls im jeweiligen Browser SIMD-Funktionalität nicht zur Verfügung steht, wird eine Tensorflow-Lite-Variante genutzt, die ohne SIMD-Operationen gebaut ist. Dazu bietet Wasm die Abfrage der zur Verfügung stehenden Features an und es kann bei der Initialisierung entsprechend darauf reagiert werden.

In Abbildung 3 ist ein Screenshot der Anwendung mit aktiviertem Weichzeichnen des Hintergrunds zu sehen. Durch die Hintergrundverarbeitung und die inzwischen selbst im mobilen Umfeld standardmäßig anzutreffenden Multi-Core-CPUs beeinträchtigt diese rechenintensive Funktionalität die Hauptanwendung in keiner Weise.

Abb. 3: Webcamvideo mit weichgezeichnetem HintergrundAbb. 3: Webcamvideo mit weichgezeichnetem Hintergrund

Fazit

Wasm wird von allen aktuellen Browsern unterstützt. Die meisten Browser stellen dabei die SIMD-Operationen der zugrunde liegenden Hardwareplattform zur Verfügung. Damit lassen sich deutlich performantere und komplexere Umsetzungen realisieren, als das allein mit JavaScript oder TypeScript möglich wäre. Allein die Wiederverwendung bestehender Bibliotheken, wie hier im Beispiel Tensorflow-Lite, zeigt, welches Potenzial sich durch Wasm für Webanwendungen ergibt. Angular liefert dabei nützliche Konzepte, die den strukturierten Einsatz von Wasm ermöglichen bzw. vereinfachen. Mit Angular sind komplexe Anwendungen mit einer gut wartbaren Architektur und guter Testbarkeit effektiv umsetzbar. Das wirkt sich positiv auf die Langlebigkeit aus und hilft so, Investments zu sichern. Wasm liefert dazu einen wichtigen Beitrag, da bestehender Programmcode für das Web verfügbar gemacht werden kann.

Wasm eignet sich jedoch nicht für alle denkbaren Anwendungsfälle. So ist zum heutigen Stand der Zugriff auf das Browser-DOM oder die direkte Netzwerkkommunikation nicht möglich. Da diese aber durch den Browser – und auch von Angular – in optimierter Weise bereitgestellt werden, ergeben sich keine gravierenden Einschränkungen.

Stay tuned

Immer auf dem Laufenden bleiben! Alle News & Updates:

Links & Literatur
Immer auf dem Laufenden bleiben!
Alle News & Updates: